快速上手:制作第一个游戏

您所在的位置:网站首页 cocos creator游戏破解 快速上手:制作第一个游戏

快速上手:制作第一个游戏

2024-07-16 13:29| 来源: 网络整理| 查看: 265

快速上手:制作第一个游戏 ​

Cocos Creator 编辑器的强大之处就是可以让开发者快速的制作游戏原型。

下面我们将跟随教程制作一款名叫 一步两步 的魔性小游戏。这款游戏考验玩家的反应能力,根据路况选择是要跳一步还是跳两步,“一步两步,一步两步,一步一步似爪牙似魔鬼的步伐”。

可以在 这里 体验一下游戏的完成形态。

新建项目 ​

如果您还不了解如何获取和启动 Cocos Creator,请阅读 安装和启动 一节。

首先启动 Cocos Creator,然后新建一个名为 MindYourStep 的项目,如果不知道如何创建项目,请阅读 Hello World!。

新建项目后会看到如下的编辑器界面:

创建游戏场景 ​

在 Cocos Creator 中,游戏场景(Scene) 是开发时组织游戏内容的中心,也是呈现给玩家所有游戏内容的载体。游戏场景中一般会包括以下内容:

场景物体角色UI以组件形式附加在场景节点上的游戏逻辑脚本

当玩家运行游戏时,就会载入游戏场景,游戏场景加载后就会自动运行所包含组件的游戏脚本,实现各种各样开发者设置的逻辑功能。所以除了资源以外,游戏场景是一切内容创作的基础。现在,让我们来新建一个场景。

在 资源管理器 中点击选中 assets 目录,点击 资源管理器 左上角的加号按钮,选择文件夹,命名为 Scenes。

点击选中 Scenes 目录(下图把一些常用的文件夹都提前创建好了),点击鼠标右键,在弹出的菜单中选择 场景文件。

我们创建了一个名叫 New Scene 的场景文件,创建完成后场景文件 New Scene 的名称会处于编辑状态,将它重命名为 Main。

双击 Main,就会在 场景编辑器 和 层级管理器 中打开这个场景。

添加跑道 ​

我们的主角需要在一个由方块(Block)组成的跑道上从屏幕左边向右边移动。我们使用编辑器自带的立方体(Cube)来组成道路。

在 层级管理器 中创建一个立方体(Cube),并命名为 Cube。

选中 Cube,按 Ctrl + D 复制出 3 个 Cube。

将 3 个 Cube 按以下坐标排列:

第一个节点位置(0,-1.5,0)第二个节点位置(1,-1.5,0)第三个节点位置(2,-1.5,0)

效果如下:

添加主角 ​创建主角节点 ​

首先创建一个名为 Player 的空节点,然后在这个空节点下创建名为 Body 的主角模型节点,为了方便,我们采用编辑器自带的胶囊体模型做为主角模型。

分为两个节点的好处是,我们可以使用脚本控制 Player 节点来使主角进行水平方向移动,而在 Body 节点上做一些垂直方向上的动画(比如原地跳起后下落),两者叠加形成一个跳越动画。

然后将 Player 节点设置在(0,0,0)位置,使得它能站在第一个方块上。效果如下:

编写主角脚本 ​

想要主角影响鼠标事件来进行移动,我们就需要编写自定义的脚本。如果您从没写过程序也不用担心,我们会在教程中提供所有需要的代码,只要复制粘贴到正确的位置就可以了,之后这部分工作可以找您的程序员小伙伴来解决。下面让我们开始创建驱动主角行动的脚本吧。

创建脚本 ​

如果还没有创建 Scripts 文件夹,首先在 资源管理器 中右键点击 assets 文件夹,选择 新建 -> 文件夹,重命名为 Scripts。

右键点击 Scripts 文件夹,选择 新建 -> TypeScript,创建一个 TypeScript 脚本,有关 TypeScript 资料可以查看 TypeScript 官方网站。

将新建脚本的名字改为 PlayerController,双击这个脚本,打开代码编辑器(例如 VSCode)。

注意:Cocos Creator 中脚本名称就是组件的名称,这个命名是大小写敏感的!如果组件名称的大小写不正确,将无法正确通过名称使用组件!

编写脚本代码 ​

在打开的 PlayerController 脚本里已经有了预先设置好的一些代码块,如下所示:

tsimport { _decorator, Component } from 'cc'; const { ccclass, property } = _decorator; @ccclass("PlayerController") export class PlayerController extends Component { /* class member could be defined like this */ // dummy = ''; /* use `property` decorator if your want the member to be serializable */ // @property // serializableDummy = 0; start () { // Your initialization goes here. } // update (deltaTime: number) { // // Your update function goes here. // } }

这些代码就是编写一个组件(脚本)所需的结构。其中,继承自 Component 的脚本称之为 组件(Component),它能够挂载到场景中的节点上,用于控制节点的行为,更详细的脚本信息可以查看 脚本。

我们在脚本 PlayerController 中添加对鼠标事件的监听,让 Player 动起来:

tsimport { _decorator, Component, Vec3, systemEvent, SystemEvent, EventMouse, Animation } from 'cc'; const { ccclass, property } = _decorator; @ccclass("PlayerController") export class PlayerController extends Component { /* class member could be defined like this */ // dummy = ''; /* use `property` decorator if your want the member to be serializable */ // @property // serializableDummy = 0; // for fake tween // 是否接收到跳跃指令 private _startJump: boolean = false; // 跳跃步长 private _jumpStep: number = 0; // 当前跳跃时间 private _curJumpTime: number = 0; // 每次跳跃时常 private _jumpTime: number = 0.1; // 当前跳跃速度 private _curJumpSpeed: number = 0; // 当前角色位置 private _curPos: Vec3 = new Vec3(); // 每次跳跃过程中,当前帧移动位置差 private _deltaPos: Vec3 = new Vec3(0, 0, 0); // 角色目标位置 private _targetPos: Vec3 = new Vec3(); start () { // Your initialization goes here. systemEvent.on(SystemEvent.EventType.MOUSE_UP, this.onMouseUp, this); } onMouseUp(event: EventMouse) { if (event.getButton() === 0) { this.jumpByStep(1); } else if (event.getButton() === 2) { this.jumpByStep(2); } } jumpByStep(step: number) { if (this._startJump) { return; } this._startJump = true; this._jumpStep = step; this._curJumpTime = 0; this._curJumpSpeed = this._jumpStep / this._jumpTime; this.node.getPosition(this._curPos); Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); } update (deltaTime: number) { if (this._startJump) { this._curJumpTime += deltaTime; if (this._curJumpTime > this._jumpTime) { // end this.node.setPosition(this._targetPos); this._startJump = false; } else { // tween this.node.getPosition(this._curPos); this._deltaPos.x = this._curJumpSpeed * deltaTime; Vec3.add(this._curPos, this._curPos, this._deltaPos); this.node.setPosition(this._curPos); } } } }

现在我们可以把 PlayerController 组件添加到主角节点 Player 上。在 层级管理器 中选中 Player 节点,然后在 属性检查器 中点击 添加组件 按钮,选择 添加用户脚本组件 -> PlayerController,为主角节点添加 PlayerController 组件。

为了能在运行时看到物体,我们需要将场景中 Camera 的参数进行一些调整,Position 设置为(0,0,13),Color 设置为(50,90,255,255):

然后点击工具栏中心位置的 Play 按钮:

在打开的网页中点击鼠标左键和右键,可以看到如下画面:

更多的预览功能,可以参考 项目预览调试

添加角色动画 ​

从上面运行的结果可以看到单纯对 Player 进行水平方向的移动是十分呆板的,我们要让 Player 跳跃起来才比较有感觉,可以通过为 Player 添加垂直方向的动画来达到这个效果。有关 动画编辑器 的更多信息,请阅读 动画编辑器

选中场景中的 Body 节点,然后在编辑器下方的 动画编辑器 中添加 Animation 组件并创建 Clip,命名为 oneStep。

进入动画编辑模式,添加 position 属性轨道,并添加三个关键帧,position 值分别为(0,0,0)、(0,0.5,0)、(0,0,0)。

注意:退出动画编辑模式前记得要保存动画,否则做的动画就白费了。

我们还可以通过 资源管理器 来创建 Clip。创建一个名为 twoStep 的 Clip 并将它添加到 Body 的 Animation 上,这里为了录制方便调整了一下面板布局。

进入动画编辑模式,选择并编辑 twoStep 的 Clip,类似第 2 步,添加三个 position 的关键帧,分别为(0,0,0)、(0,1,0)、(0,0,0)。

在 PlayerController 组件中引用 动画组件,我们需要在代码中根据跳的步数不同来播放不同的动画。

首先需要在 PlayerController 组件中引用 Body 身上的 Animation。

ts@property({type: Animation}) public BodyAnim: Animation | null = null;

然后在 属性检查器 中将 Body 身上的 Animation 拖到这个变量上。

在跳跃的函数 jumpByStep 中加入动画播放的代码:

tsif (this.BodyAnim) { if (step === 1) { this.BodyAnim.play('oneStep'); } else if (step === 2) { this.BodyAnim.play('twoStep'); } }

最后点击 Play 按钮,点击鼠标左键和右键,可以看到新的跳跃效果:

跑道升级 ​

为了让游戏有更久的生命力,我们需要一个很长的跑道让 Player 在上面一直往右跑。在场景中复制一堆 Cube 并编辑位置来组成跑道显然不是一个明智的做法,我们可以通过脚本完成跑道的自动创建。

游戏管理器(GameManager) ​

一般游戏都会有一个管理器,主要负责整个游戏生命周期的管理,可以将跑道的动态创建代码放到这里。在场景中创建一个名为 GameManager 的节点,然后在 assets/Scripts 中创建一个名为 GameManager 的 TypeScript 脚本文件,并将它添加到 GameManager 节点上。

制作Prefab ​

对于需要重复生成的节点,我们可以将它保存成 Prefab(预制)资源,作为我们动态生成节点时使用的模板。

将生成跑道的基本元素 正方体(Cube) 制作成 Prefab,之后可以把场景中的三个 Cube 都删除了。

添加自动创建跑道代码 ​

Player 需要一个很长的跑道,理想的方法是能动态增加跑道的长度,这样可以永无止境地跑下去,这里为了方便先生成一个固定长度的跑道,跑道长度可以自己定义。另外,我们可以在跑道上生成一些坑,当 Player 跳到坑上就 GameOver 了。

将 GameManager 脚本中的代码替换成以下代码:

tsimport { _decorator, Component, Prefab, instantiate, Node, CCInteger } from 'cc'; const { ccclass, property } = _decorator; // 赛道格子类型,坑(BT_NONE)或者实路(BT_STONE) enum BlockType { BT_NONE, BT_STONE, }; @ccclass("GameManager") export class GameManager extends Component { // 赛道预制 @property({type: Prefab}) public cubePrfb: Prefab | null = null; // 赛道长度 @property public roadLength = 50; private _road: BlockType[] = []; start () { this.generateRoad(); } generateRoad() { // 防止游戏重新开始时,赛道还是旧的赛道 // 因此,需要移除旧赛道,清除旧赛道数据 this.node.removeAllChildren(); this._road = []; // 确保游戏运行时,人物一定站在实路上 this._road.push(BlockType.BT_STONE); // 确定好每一格赛道类型 for (let i = 1; i = this.roadLength ? this.roadLength : moveIndex); } this.checkResult(moveIndex); }光照和阴影 ​

有光的地方就会有影子,光和影构成明暗交错的 3D 世界。接下来我们为角色加上简单的影子。

开启阴影 ​

在 层级管理器 中点击最顶部的 Scene 节点,然后在 属性检查器 勾选 shadows 中的 Enabled,并修改 Distance 和 Normal 属性:

点击 Player 节点下的 Body 节点,将 cc.MeshRenderer 组件中的 ShadowCastingMode 设置为 ON。

此时在 场景编辑器 中会看到一个阴影面片,预览会发现看不到这个阴影,这是因为它在模型的正后方,被胶囊体盖住了。

调整光照 ​

新建场景时默认会添加一个挂载了 cc.DirectionalLight 组件的 Main Light 节点,由这个平行光计算阴影。所以为了让阴影换个位置显示,我们可以调整这个平行光的方向。在 层级管理器 中点击选中 Main Light 节点,调整 Rotation 属性为(-10,17,0)。

点击预览可以看到影子效果:

添加主角模型 ​

做为一个官方教程,用胶囊体当主角显的有点寒碜,所以我们花(低)重(预)金(算)制作了一个 Cocos 主角。

导入模型资源 ​

从原始资源导入模型、材质、动画等资源不是本篇基础教程的重点,所以这边直接使用已经导入工程的资源。将 项目工程(GitHub | Gitee)中 assets 目录下的 cocos 文件夹拷贝到你自己工程的 assets 目录下。

添加到场景中 ​

在 cocos 文件中已经包含了一个名为 Cocos 的 Prefab,将它拖拽到 层级管理器 中 Player 节点下的 Body 节点中,作为 Body 节点的子节点。

同时在 属性检查器 中移除原先的胶囊体模型:

此时会发现模型有些暗,可以在 Cocos 节点下加个聚光灯(Spotlight),以突出它锃光瓦亮的脑门。

添加跳跃动画 ​

现在预览可以看到主角初始会有一个待机动画,但是跳跃时还是用这个待机动画会显得很不协调,所以我们可以在跳跃过程中将其换成跳跃的动画。在 PlayerController.ts 类中添加一个引用模型动画的变量:

ts@property({type: SkeletalAnimation}) public CocosAnim: SkeletalAnimation|null = null;

同时,因为我们将主角从胶囊体换成了人物模型,可以弃用之前为胶囊体制作的动画,并注释相关代码:

ts// @property({type: Animation}) // public BodyAnim: Animation|null = null; jumpByStep(step: number) { // ... // if (this.BodyAnim) { // if (step === 1) { // this.BodyAnim.play('oneStep'); // } else if (step === 2) { // this.BodyAnim.play('twoStep'); // } // } }

然后在 层级管理器 中将 Cocos 节点拖拽到 Player 节点的 CocosAnim 属性框中:

在 PlayerController 脚本的 jumpByStep 函数中播放跳跃动画:

tsjumpByStep(step: number) { if (this._startJump) { return; } this._startJump = true; this._jumpStep = step; this._curJumpTime = 0; this._curJumpSpeed = this._jumpStep / this._jumpTime; this.node.getPosition(this._curPos); Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); if (this.CocosAnim) { this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; // 跳跃动画时间比较长,这里加速播放 this.CocosAnim.play('cocos_anim_jump'); // 播放跳跃动画 } // if (this.BodyAnim) { // if (step === 1) { // this.BodyAnim.play('oneStep'); // } else if (step === 2) { // this.BodyAnim.play('twoStep'); // } // } this._curMoveIndex += step; }

在 PlayerController 脚本的 onOnceJumpEnd 函数中让主角变为待机状态,播放待机动画。

tsonOnceJumpEnd() { if (this.CocosAnim) { this.CocosAnim.play('cocos_anim_idle'); } this.node.emit('JumpEnd', this._curMoveIndex); }

预览效果如下:

最终代码 ​

PlayerController.ts

tsimport { _decorator, Component, Vec3, systemEvent, SystemEvent, EventMouse, Animation, SkeletalAnimation } from 'cc'; const { ccclass, property } = _decorator; @ccclass("PlayerController") export class PlayerController extends Component { @property({type: Animation}) public BodyAnim: Animation|null = null; @property({type: SkeletalAnimation}) public CocosAnim: SkeletalAnimation|null = null; // for fake tween private _startJump: boolean = false; private _jumpStep: number = 0; private _curJumpTime: number = 0; private _jumpTime: number = 0.3; private _curJumpSpeed: number = 0; private _curPos: Vec3 = new Vec3(); private _deltaPos: Vec3 = new Vec3(0, 0, 0); private _targetPos: Vec3 = new Vec3(); private _curMoveIndex = 0; start () { } reset() { this._curMoveIndex = 0; } setInputActive(active: boolean) { if (active) { systemEvent.on(SystemEvent.EventType.MOUSE_UP, this.onMouseUp, this); } else { systemEvent.off(SystemEvent.EventType.MOUSE_UP, this.onMouseUp, this); } } onMouseUp(event: EventMouse) { if (event.getButton() === 0) { this.jumpByStep(1); } else if (event.getButton() === 2) { this.jumpByStep(2); } } jumpByStep(step: number) { if (this._startJump) { return; } this._startJump = true; this._jumpStep = step; this._curJumpTime = 0; this._curJumpSpeed = this._jumpStep / this._jumpTime; this.node.getPosition(this._curPos); Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); if (this.CocosAnim) { this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; //跳跃动画时间比较长,这里加速播放 this.CocosAnim.play('cocos_anim_jump'); //播放跳跃动画 } // if (this.BodyAnim) { // if (step === 1) { // this.BodyAnim.play('oneStep'); // } else if (step === 2) { // this.BodyAnim.play('twoStep'); // } // } this._curMoveIndex += step; } onOnceJumpEnd() { if (this.CocosAnim) { this.CocosAnim.play('cocos_anim_idle'); } this.node.emit('JumpEnd', this._curMoveIndex); } update (deltaTime: number) { if (this._startJump) { this._curJumpTime += deltaTime; if (this._curJumpTime > this._jumpTime) { // end this.node.setPosition(this._targetPos); this._startJump = false; this.onOnceJumpEnd(); } else { // tween this.node.getPosition(this._curPos); this._deltaPos.x = this._curJumpSpeed * deltaTime; Vec3.add(this._curPos, this._curPos, this._deltaPos); this.node.setPosition(this._curPos); } } } }

GameManager.ts

tsimport { _decorator, Component, Prefab, instantiate, Node, Label, CCInteger, Vec3 } from 'cc'; import { PlayerController } from "./PlayerController"; const { ccclass, property } = _decorator; // 赛道格子类型,坑(BT_NONE)或者实路(BT_STONE) enum BlockType{ BT_NONE, BT_STONE, }; enum GameState{ GS_INIT, GS_PLAYING, GS_END, }; @ccclass("GameManager") export class GameManager extends Component { // 赛道预制 @property({type: Prefab}) public cubePrfb: Prefab | null = null; // 赛道长度 @property public roadLength = 50; private _road: BlockType[] = []; // 主界面根节点 @property({type: Node}) public startMenu: Node | null = null; // 关联 Player 节点身上 PlayerController 组件 @property({type: PlayerController}) public playerCtrl: PlayerController | null = null; // 关联步长文本组件 @property({type: Label}) public stepsLabel: Label = null!; start () { this.curState = GameState.GS_INIT; this.playerCtrl.node.on('JumpEnd', this.onPlayerJumpEnd, this); } init() { // 激活主界面 if (this.startMenu) { this.startMenu.active = true; } // 生成赛道 this.generateRoad(); if(this.playerCtrl){ // 禁止接收用户操作人物移动指令 this.playerCtrl.setInputActive(false); // 重置人物位置 this.playerCtrl.node.setPosition(Vec3.ZERO); // 重置已经移动的步长数据 this.playerCtrl.reset(); } } set curState (value: GameState) { switch(value) { case GameState.GS_INIT: this.init(); break; case GameState.GS_PLAYING: this.startMenu.active = false; this.stepsLabel.string = '0'; // 将步数重置为0 // 设置 active 为 true 时会直接开始监听鼠标事件,此时鼠标抬起事件还未派发 // 会出现的现象就是,游戏开始的瞬间人物已经开始移动 // 因此,这里需要做延迟处理 setTimeout(() => { this.playerCtrl.setInputActive(true); }, 0.1); break; case GameState.GS_END: break; } } generateRoad() { // 防止游戏重新开始时,赛道还是旧的赛道 // 因此,需要移除旧赛道,清除旧赛道数据 this.node.removeAllChildren(); this._road = []; // 确保游戏运行时,人物一定站在实路上 this._road.push(BlockType.BT_STONE); // 确定好每一格赛道类型 for (let i = 1; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3